﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Net;
using System.Reflection;
using log4net;

namespace Ping.Server
{
	public sealed class Blacklist
	{
		private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

		public sealed class Block
		{
			private Block(DateTime date)
				: this()
			{
				if (date <= DateTime.Now)
					throw new ArgumentException("Blocking an address is only useful for dates in the future");

				UntilDate = date;
			}

			private Block()
			{
				From = DateTime.UtcNow;
			}

			/// <summary>
			/// Creates an entry for an IP-Address that blocks it for eternity.
			/// </summary>
			public static Block Forever()
			{
				return new Block();
			}

			/// <summary>
			/// Creates an entry for an IP-Address that blocks it until the given date.
			/// </summary>
			/// <param name="date">The date at which the address becomes unblocked again</param>
			public static Block Until(DateTime date)
			{
				return new Block(date);
			}

			public override string ToString()
			{
				return UntilDate == null
				       	? "Eternity"
				       	: UntilDate.Value.ToString();
			}

			public readonly DateTime From;

			/// <summary>
			/// The date, until which this entry is blacklisted.
			/// </summary>
			public readonly DateTime? UntilDate;

			public bool IsUnblocked(DateTime now)
			{
				return UntilDate == null
				       	? false
				       	: now > UntilDate;
			}
		}

		private readonly Dictionary<IPAddress, Block> _addresses;

		public Blacklist()
		{
			_addresses = new Dictionary<IPAddress, Block>();

			// For some reason, my gateway keeps establishing connections with this server
			// so it's being blocked...
			Add("192.168.11.1");
		}

		public int NumBlocked
		{
			get { lock(_addresses) return _addresses.Count; }
		}

		/// <summary>
		/// Blocks the given address FOREVER.
		/// </summary>
		/// <param name="address"></param>
		public void Add(string address)
		{
			Add(IPAddress.Parse(address));
		}

		/// <summary>
		/// Blocks the given address FOREVER.
		/// </summary>
		/// <param name="address"></param>
		public void Add(IPAddress address)
		{
			Add(address, Block.Forever());
		}

		public void Add(IPAddress address, Block block)
		{
			if (address == null)
				throw new ArgumentNullException();
			if (block == null)
				throw new ArgumentNullException();

			Log.InfoFormat("Blocking IP-Address {0} until {1}", address, block);

			_addresses.Add(address, block);
		}

		public bool IsBlocked(IPAddress address)
		{
			lock (_addresses)
			{
				Block e;
				if (!_addresses.TryGetValue(address, out e))
					return false;

				if (e.UntilDate == null)
					return true; //< Blocked until eternity

				// No longer blocked
				return DateTime.Now > e.UntilDate.Value;
			}
		}

		/// <summary>
		/// Removes all entries from the blacklist that are no longer blocked.
		/// </summary>
		public void Cleanup()
		{
			lock (_addresses)
			{
				var now = DateTime.Now;

				// Step #1: Collect all entries that are too old
				var toRemove = (from pair in _addresses
				                where pair.Value.IsUnblocked(now)
				                select pair.Key).ToArray();

				Log.InfoFormat("Removing {0} entries from the blacklist", toRemove.Length);

				// Step #2: Remove old entries
				foreach(var key in toRemove)
				{
					_addresses.Remove(key);
				}
			}
		}
	}
}